﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Diagnostics.Contracts;
using System.IO;

namespace RPGMapRevealer
{
    /// <summary>
    /// Displays a Map and all the additional information on it
    /// </summary>
    public partial class MapPreviewUserControl : UserControl
    {
        public bool WallEditMode
        { get; set; }

        /// <summary>
        /// Constructs a Map Preview Control
        /// </summary>
        public MapPreviewUserControl()
        {
            InitializeComponent();
            loadDefaultImages();
            setDefaultClickActions();
            CustomLightLevel = 8;
        }

        private void setDefaultClickActions()
        {
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.Shift, Button = MouseButtons.Left, Action = ClickAction.ToggleWallBlocksLOS });
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.Control, Button = MouseButtons.Left, Action = ClickAction.ToggleSecret });
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.None, Button = MouseButtons.Middle, Action = ClickAction.ToggleSecret });
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.None, Button = MouseButtons.Left, Action = ClickAction.ToggleLightCustom });
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.None, Button = MouseButtons.Right, Action = ClickAction.MovePlayerMarker1 });
            _clickHandlers.Add(new ClickHandler() { Keys = Keys.None, Button = MouseButtons.Left, Action = ClickAction.ToggleWallBlocksLOS, WallEditMode = true });            
        }

        private void loadDefaultImages()
        {
            SetSecretIcons(LoadImage("Icons/secret-hidden.png"), LoadImage("Icons/secret-revealed.png"));
            SetLightSourceIcon(5, LoadImage("Icons/lightbulb.png"));
            LineOfSightIcons.Add(LoadImage("Icons/player-1.png"));
            LineOfSightIcons.Add(LoadImage("Icons/player-2.png"));
            LineOfSightIcons.Add(LoadImage("Icons/player-3.png"));
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static Image LoadImage(String filename, String path = null)
        {
            Contract.Requires(filename != null);
            try
            {
                String p = path != null ? path : Application.StartupPath ?? "";
                String fullname = Path.Combine(p, filename);
                if (File.Exists(fullname))
                {
                    return Bitmap.FromFile(fullname);
                }
            }
            catch (Exception) { }
            return null;
        }

        /// <summary>
        /// The Map object this window is displaying
        /// </summary>
        public MapInfo MapInfo
        {
            get { return _mapInfo; }
            set
            {
                if (value == null || value.PublicImage == null)
                {
                    _mapInfo = null;
                    return;
                }
                _mapInfo = value;
                this.Width = value.PublicImage.Width;
                this.Height = value.PublicImage.Height;
                this.Refresh();
            }
        }
        private MapInfo _mapInfo = null;

        /// <summary>
        /// Displays the Map and any added information such as secrets, lightlevel, tokens, etc
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPaint(PaintEventArgs e)
        {
            Contract.Assert(e != null);
            base.OnPaint(e);
            if (_mapInfo == null)
            { return; }

            e.Graphics.DrawImage(_mapInfo.PublicImage, 0, 0, _mapInfo.PublicImage.Width, _mapInfo.PublicImage.Height);
            double alpha = (animationFrames[animationCounter] / 100d);
            if (_mapInfo.SecretImage != null && alpha > 0)
            {
                ColorMatrix alphaTransform = new ColorMatrix();
                alphaTransform.Matrix33 = (float)alpha;
                using (ImageAttributes attr = new ImageAttributes())
                {
                    attr.SetColorMatrix(alphaTransform);
                    Rectangle fullImage = new Rectangle(0, 0, _mapInfo.SecretImage.Width, _mapInfo.SecretImage.Height);
                    e.Graphics.DrawImage(_mapInfo.SecretImage, fullImage, 0, 0, fullImage.Width, fullImage.Height, GraphicsUnit.Pixel, attr);
                }
            }
            DrawTileIcons(e.Graphics);
        }

        private void DrawTileIcons(Graphics g)
        {
            Contract.Requires(g != null);
            if (MapInfo == null)
            { return; }
            List<AllBorders> borders = new List<AllBorders>();
            GridConfig grid = MapInfo.GridConfig;
            foreach (Tile tile in MapInfo.Tiles)
            {
                if (tile == null) { continue; }
                borders.Add(DrawTileIcon(tile, g, new Rectangle(tile.X * grid.TileWidth + grid.TileOffsetX, tile.Y * grid.TileHeight + grid.TileOffsetY, grid.TileWidth, grid.TileHeight)));
            }
            foreach (AllBorders borderInfo in borders)
            {
                DrawBorders(borderInfo, g);
            }
        }

        private class AllBorders
        {
            public BorderDesc noAdjacent { get; set; }
            public BorderDesc top { get; set; }
            public BorderDesc left { get; set; }
            public BorderDesc right { get; set; }
            public BorderDesc bottom { get; set; }
            public Rectangle r { get; set; }
        }

        private class BorderDesc
        {
            public bool IsSecretHiddenBorder;
            public bool IsSecretRevealedBorder;
            public bool IsLightBorder;
            public bool IsBlocksLOSBorder;
        }

        private AllBorders DrawTileIcon(Tile tile, Graphics g, Rectangle r)
        {
            Contract.Requires(tile != null);
            Contract.Requires(g != null);
            Contract.Requires(r != null);

            AllBorders borderInfo = new AllBorders();
            borderInfo.noAdjacent = CompareTileBorders(tile);
            borderInfo.top = borderInfo.noAdjacent;
            borderInfo.left = borderInfo.noAdjacent;
            borderInfo.right = borderInfo.noAdjacent;
            borderInfo.bottom = borderInfo.noAdjacent;
            borderInfo.r = r;

            int tx = tile.X;
            int ty = tile.Y;
            foreach (Tile t in tile.AdjacentTiles)
            {
                if (t == null) { continue; }
                BorderDesc border = CompareTileBorders(tile, t);
                if (tx == t.X)
                {
                    if (t.Y == ty - 1)
                    { borderInfo.top = border; }
                    else if (t.Y == ty + 1)
                    { borderInfo.bottom = border; }
                }
                else if (ty == t.Y)
                {
                    if (t.X == tx - 1)
                    { borderInfo.left = border; }
                    else if (t.X == tx + 1)
                    { borderInfo.right = border; }
                }
            }
            if (!WallEditMode)
            {
                if (tile.HasSecret)
                {
                    if (tile.IsRevealed && _secretRevealedIcon != null)
                    {
                        g.DrawImage(_secretRevealedIcon, r);
                    }
                    else if (!tile.IsRevealed && _secretHiddenIcon != null)
                    {
                        g.DrawImage(_secretHiddenIcon, r);
                    }
                }
                if (tile.IsPlayerPresent)
                {
                    int first = Math.Max(1, MapInfo.PlayersAtPosition(tile).FirstOrDefault());
                    first = Math.Min(LineOfSightIcons.Count, first);
                    first -= 1;
                    g.DrawImage(LineOfSightIcons[first], r);
                }
                if (tile.LightSource > 0)
                {
                    int lastLevel = _lightLevels.FirstOrDefault();
                    foreach (int level in _lightLevels)
                    {
                        if (level <= tile.LightSource)
                        {
                            lastLevel = level;
                        }
                    }
                    Image icon;
                    if (_lightIcons.TryGetValue(lastLevel, out icon))
                    {
                        g.DrawImage(icon, r);
                    }
                }

                int alpha = (int)((double)(10 - tile.LightLevel) / 10 * 255 * .5);
                alpha = Math.Min(255, Math.Max(0, alpha));
                using (Brush lightBrush = new SolidBrush(Color.FromArgb(alpha, Color.Black)))
                {
                    g.FillRectangle(lightBrush, r);
                }
            }
            return borderInfo;
        }

        private void DrawBorders(AllBorders borderInfo, Graphics g)
        {
            Rectangle r = borderInfo.r;
            if (borderInfo.top != null)
            { DrawBorder(borderInfo.top, g, r.Left, r.Top, r.Right, r.Top, 0, -1); }
            if (borderInfo.bottom != null)
            { DrawBorder(borderInfo.bottom, g, r.Left, r.Bottom, r.Right, r.Bottom, 0, +1); }
            if (borderInfo.left != null)
            { DrawBorder(borderInfo.left, g, r.Left, r.Top, r.Left, r.Bottom, 1, 0); }
            if (borderInfo.right != null)
            { DrawBorder(borderInfo.right, g, r.Right, r.Top, r.Right, r.Bottom, -1, 0); }
        }

        private void DrawBorder(BorderDesc border, Graphics g, int x1, int y1, int x2, int y2, int dx, int dy)
        {
            Contract.Requires(border != null);
            Contract.Requires(g != null);
            if (!WallEditMode)
            {
                if (border.IsSecretHiddenBorder)
                {
                    g.DrawLine(Pens.Red, x1, y1, x2, y2);
                    x1 += dx;
                    x2 += dx;
                    y1 += dy;
                    y2 += dy;
                }
                if (border.IsSecretRevealedBorder)
                {
                    g.DrawLine(Pens.Green, x1, y1, x2, y2);
                    x1 += dx;
                    x2 += dx;
                    y1 += dy;
                    y2 += dy;
                }
                if (border.IsLightBorder)
                {
                    g.DrawLine(Pens.Yellow, x1, y1, x2, y2);
                    x1 += dx;
                    x2 += dx;
                    y1 += dy;
                    y2 += dy;
                }
            }
            else
            {
                if (border.IsSecretHiddenBorder || border.IsSecretRevealedBorder)
                {
                    g.DrawLine(Pens.Red, x1, y1, x2, y2);
                    x1 += dx;
                    x2 += dx;
                    y1 += dy;
                    y2 += dy;
                }
            }
            if (border.IsBlocksLOSBorder)
            {
                g.DrawLine(Pens.Magenta, x1, y1, x2, y2);
                x1 += dx;
                x2 += dx;
                y1 += dy;
                y2 += dy;
            }
            g.DrawLine(Pens.Black, x1, y1, x2, y2);
        }

        private static BorderDesc CompareTileBorders(Tile current, Tile neighbor = null)
        {
            Contract.Requires(current != null);
            BorderDesc result = new BorderDesc();
            if (neighbor == null)
            {
                result.IsLightBorder = current.LightLevel >= 5;
                result.IsSecretHiddenBorder = current.HasSecret && !current.IsRevealed;
                result.IsSecretRevealedBorder = current.HasSecret && current.IsRevealed;
            }
            else
            {
                result.IsLightBorder = current.LightLevel >= 5 && neighbor.LightLevel < 5;
                result.IsSecretHiddenBorder = (current.HasSecret && !current.IsRevealed) && (!neighbor.HasSecret || neighbor.IsRevealed);
                result.IsSecretRevealedBorder = (current.HasSecret && current.IsRevealed) && (!neighbor.HasSecret || !neighbor.IsRevealed);
                result.IsBlocksLOSBorder = current.NoLOSBetweenTiles.Contains(neighbor);
            }
            return result;
        }


        private int animationCounter = 0;
        private double[] animationFrames = new double[] { 0, 100 };
        private void timerDisplayState_Tick(object sender, EventArgs e)
        {
            if (_mapInfo == null) { return; }
            if (_mapInfo.SecretImage == null) { return; }
            double lastValue = animationFrames[animationCounter];
            animationCounter = (animationCounter + 1) % animationFrames.Length;
            double currentValue = animationFrames[animationCounter];
            if (lastValue != currentValue)
            {
                Refresh();
            }
        }

        private List<int> _lightLevels = new List<int>();
        private Dictionary<int, Image> _lightIcons = new Dictionary<int, Image>();
        /// <summary>
        /// Sets a light icon for a given light level
        /// </summary>
        /// <param name="lightLevel">The level for which this icon should apply</param>
        /// <param name="icon">the icon</param>
        public void SetLightSourceIcon(int lightLevel, Image icon)
        {
            Contract.Requires(lightLevel >= 0);
            if (icon == null) { return; }
            _lightLevels.Remove(lightLevel);
            _lightLevels.Add(lightLevel);
            _lightIcons[lightLevel] = icon;
            _lightLevels.Sort();
        }

        private List<Image> _lineOfSightIcons = new List<Image>();
        /// <summary>
        /// Player markers
        /// </summary>
        public IList<Image> LineOfSightIcons
        {
            get
            {
                Contract.Ensures(Contract.Result<IList<Image>>() != null);
                return _lineOfSightIcons;
            }
        }

        private Image _secretHiddenIcon = null;
        private Image _secretRevealedIcon = null;
        /// <summary>
        /// Sets the icons to use for hidden areas and revealed areas
        /// </summary>
        /// <param name="hidden">The icon to use when a secret area is hidden to the players</param>
        /// <param name="revealed">The icon to use when a secret area is revealed to the players</param>
        public void SetSecretIcons(Image hidden, Image revealed)
        {
            _secretHiddenIcon = hidden;
            _secretRevealedIcon = revealed;
        }

        private void MapPreviewUserControl_Click(object sender, EventArgs e)
        {

            MouseEventArgs me = e as MouseEventArgs;
            if (me == null) { return; }
            if (MapInfo == null) { return; }
            bool wem = WallEditMode;
            GridConfig grid = MapInfo.GridConfig;
            int x = (me.X - grid.TileOffsetX) / grid.TileWidth;
            int y = (me.Y - grid.TileOffsetY) / grid.TileHeight;


            Tile t = MapInfo.GetTileByXY(x, y);
            if (t == null) { return; }
            Tile t2 = null;
            double dLeft = ((double)me.X - (double)grid.TileOffsetX) % (double)grid.TileWidth;
            double dTop = ((double)me.Y - (double)grid.TileOffsetY) % (double)grid.TileHeight;
            double dRight = (double)grid.TileWidth - dLeft;
            double dBottom = (double)grid.TileHeight - dTop;
            double min = Math.Min(Math.Min(dLeft, dTop), Math.Min(dRight, dBottom));
            if (dLeft == min)
            { t2 = t.AdjacentTiles.Where(adjacent => adjacent.X == t.X - 1 && adjacent.Y == t.Y).FirstOrDefault(); }
            else if (dTop == min)
            { t2 = t.AdjacentTiles.Where(adjacent => adjacent.X == t.X && adjacent.Y == t.Y - 1).FirstOrDefault(); }
            else if (dRight == min)
            { t2 = t.AdjacentTiles.Where(adjacent => adjacent.X == t.X + 1 && adjacent.Y == t.Y).FirstOrDefault(); }
            else if (dBottom == min)
            { t2 = t.AdjacentTiles.Where(adjacent => adjacent.X == t.X && adjacent.Y == t.Y + 1).FirstOrDefault(); }

            foreach (ClickHandler handler in _clickHandlers)
            {
                if (handler == null) { continue; }
                if ((ModifierKeys & handler.Keys) == handler.Keys &&
                    me.Button == handler.Button && handler.WallEditMode == WallEditMode)
                {
                    DoAction(t, t2, handler.Action);
                    return;
                }
            }
        }

        public bool AnimatePlayerMovement { get; set; }

        private void DoAction(Tile tile, Tile nearest, ClickAction action)
        {
            Contract.Requires(tile != null);
            if (MapInfo == null)
            { return; }
            switch (action)
            {
                case ClickAction.ToggleWallBlocksLOS:
                    if (nearest == null) { return; }
                    if (tile.NoLOSBetweenTiles.Contains(nearest))
                    {
                        tile.NoLOSBetweenTiles.Remove(nearest);
                        nearest.NoLOSBetweenTiles.Remove(tile);
                    }
                    else
                    {
                        tile.NoLOSBetweenTiles.Remove(nearest);
                        nearest.NoLOSBetweenTiles.Remove(tile);
                        tile.NoLOSBetweenTiles.Add(nearest);
                        nearest.NoLOSBetweenTiles.Add(tile);
                    }
                    break;
                case ClickAction.ToggleSecret:
                    if (!tile.HasSecret) { return; }
                    bool newState = !tile.IsRevealed;
                    foreach (Tile t in tile.Search(5, false, (t => t.HasSecret && t.IsRevealed != newState)).Flatten())
                    {
                        t.IsRevealed = newState;
                    }
                    break;
                case ClickAction.MovePlayerMarker1:
                    Tile start = MapInfo.PositionOfPlayer(1);
                    if (start != null)
                    {
                        bool animate = AnimatePlayerMovement;
                        IEnumerable<Tile> path = start.FindShortestPath(tile);
                        if (path != null)
                        {
                            int count = 0;
                            foreach (Tile step in path)
                            {
                                count++;
                                if (count % 2 != 0 && !animate)
                                { continue; }
                                if (animate)
                                { PlayerFollowingPath.Enqueue(step); }
                                else
                                {
                                    MapInfo.MovePlayer(1, step);
                                    MapInfo.RecalculateLightAndLineOfSight(true);
                                }
                            }
                        }
                        if (!animate || path == null)
                        { MapInfo.MovePlayer(1, tile); }
                    }
                    else
                    { MapInfo.MovePlayer(1, tile); }
                    ScrollTileToCenter(tile);
                    break;
                case ClickAction.MovePlayerMarker2:
                    MapInfo.MovePlayer(2, tile);
                    break;
                case ClickAction.MovePlayerMarker3:
                    MapInfo.MovePlayer(3, tile);
                    break;
                case ClickAction.ToggleLightCustom:
                    tile.LightSource = tile.LightSource == CustomLightLevel ? 0 : CustomLightLevel;
                    break;
                case ClickAction.ToggleLight100:
                    tile.LightSource = tile.LightSource == 20 ? 0 : 20;
                    break;
                case ClickAction.ToggleLight50:
                    tile.LightSource = tile.LightSource == 10 ? 0 : 10;
                    break;
                case ClickAction.ToggleLight10:
                    tile.LightSource = tile.LightSource == 5 ? 0 : 5;
                    break;
                default:
                    //do nothing
                    break;
            }
            MapInfo.RecalculateLightAndLineOfSight();
            Refresh();
        }

        private void ScrollTileToCenter(Tile tile)
        {
            int x = tile.X * MapInfo.GridConfig.TileWidth;
            int y = tile.Y * MapInfo.GridConfig.TileHeight;
            x -= Parent.ClientSize.Width / 2;
            y -= Parent.ClientSize.Height / 2;

            if (Parent is Panel)
            { ((Panel)Parent).AutoScrollPosition = new Point(x, y); }
        }

        public int CustomLightLevel { get; set; }

        private List<ClickHandler> _clickHandlers = new List<ClickHandler>();

        private class ClickHandler
        {
            public Keys Keys { get; set; }
            public MouseButtons Button { get; set; }
            public ClickAction Action { get; set; }
            public bool WallEditMode { get; set; }
        }

        private enum ClickAction
        {
            ToggleLightCustom,
            ToggleLight100,
            ToggleLight50,
            ToggleLight10,
            MovePlayerMarker1,
            MovePlayerMarker2,
            MovePlayerMarker3,
            ToggleSecret,
            ToggleWallBlocksLOS
        }

        [ContractInvariantMethod]
        private void InvariantMethod()
        {
            Contract.Invariant(animationCounter >= 0);
            Contract.Invariant(animationFrames != null);
            Contract.Invariant(animationCounter < animationFrames.Length);
            Contract.Invariant(timerDisplayState != null);
            Contract.Invariant(_lightIcons != null);
            Contract.Invariant(_lightLevels != null);
            Contract.Invariant(_lineOfSightIcons != null);
            Contract.Invariant(_clickHandlers != null);
        }

        private Queue<Tile> PlayerFollowingPath = new Queue<Tile>();

        private void timerAnimatePlayerPosition_Tick(object sender, EventArgs e)
        {
            if (PlayerFollowingPath.Count != 0)
            {
                MapInfo.MovePlayer(1, PlayerFollowingPath.Dequeue());
                MapInfo.RecalculateLightAndLineOfSight();
                Refresh();
            }
        }

    }
}
